<?php
/**
 * @link https://ym2.cleverstone.top
 * @copyright Copyright (c) 2020 Yii Manager Software LLC
 */

namespace builder\tree;

use Yii;
use yii\web\View;
use yii\web\AssetBundle;
use yii\helpers\ArrayHelper;
use yii\base\InvalidArgumentException;
use builder\helper\H5;
use builder\helper\RegEx;
use builder\base\Builder;
use builder\helper\Url;
use builder\base\ViewBuilderInterface;
use builder\tree\util\oitem\OperationType;
use builder\helper\App;

/**
 * 树组件构建器
 *
 * ------------------- html -------------------
 * @property bool $partial 是否局部继承
 * @property string $ajaxRoute AJAX请求路由
 * @property string $title 页标题
 * @property array $rootLayout 根布局
 * @property array $treeLayout 树布局
 * @property boolean $showCheckbox 是否显示复选框
 * @property boolean $accordion 是否开启手风琴模式
 * @property boolean $onlyIconControl 是否仅允许节点左侧图标控制展开伸缩
 * @property boolean $isJump 是否允许点击节点时弹出新窗口跳转。若为 true，则需在对应的 data 中设定 href 属性（url 格式）
 * @property boolean $showLine 是否开启节点连接线。若设为 false，则节点左侧出现三角图标。
 * @property array $customName 自定义 data 数据源中常用的字段名称。
 * @property boolean $switchCheckedBtn 是否显示全选/反选按钮
 * @property boolean $switchSpreadBtn 是否显示展开/折叠按钮
 * @property array $operationItem 自定义操作项
 * @property array $extraJs 额外的JS
 * @property array $extraCss 额外的CSS
 * @property array $extraAssetBundle 额外的静态包
 * @property boolean $autoRequest 是否自动发起数据请求
 *
 * ------------------- ajax -------------------
 * @property array $data Ajax返回数据
 * @author cleverstone
 * @since ym2.0
 */
class TreeBuilder extends Builder implements ViewBuilderInterface
{
    /**
     * The location of registered html or text.
     * This means the location is in the tree head section.
     */
    const POS_HEAD = 1;

    /**
     * The location of registered html or text.
     * This means the location is at the end of the tree section.
     */
    const POS_END = 2;

    /**
     * @var bool 是否局部继承
     * @see $partial
     * @see setPartial()
     * @see getPartial()
     */
    private $_partial = false;

    /**
     * @var array 表格AJAX路由
     * - route string 路由
     * - params array 路由参数
     *
     * @see $ajaxRoute
     * @see setAjaxRoute()
     * @see getAjaxRoute()
     */
    private $_ajaxRoute = [];

    /**
     * @var string 标题
     * @see $title
     * @see setTitle()
     * @see getTitle()
     */
    private $_title = '';

    /**
     * @var array 根布局
     * @see $rootLayout
     * @see setRootLayout()
     * @see getRootLayout()
     */
    private $_rootLayout = [];

    /**
     * @var array 树布局
     * @see $treeLayout
     * @see setTreeLayout()
     * @see getTreeLayout()
     */
    private $_treeLayout = [];

    /**
     * @var array|\Closure Ajax数据
     * 数据格式如下:
     * - title string 节点标题 -
     * - id string 节点唯一索引值，用于对指定节点进行各类操作 -
     * - field string 节点字段名 -
     * - children array 子节点。支持设定属性选项同父节点 -
     * - href string 点击节点弹出新窗口对应的 url。需开启 isJump 基础属性才有效。 -
     * - spread boolean 节点是否初始展开 false
     * - checked boolean 节点是否初始为选中状态。需开启 showCheckbox 基础属性时有效。 false
     * - disabled boolean 节点是否为禁用状态 false
     *
     * @see $data
     * @see setData()
     * @see getData()
     */
    private $_data;

    /**
     * @var bool 是否显示复选框
     * @see $showCheckbox
     * @see setShowCheckbox()
     * @see getShowCheckbox()
     */
    private $_showCheckbox = true;

    /**
     * @var bool 是否开启手风琴模式
     * @see $accordion
     * @see setAccordion()
     * @see getAccordion()
     */
    private $_accordion = false;

    /**
     * @var bool 是否仅允许节点左侧图标控制展开伸缩。
     * 默认为 false，即点击节点本身也可控制伸缩
     * 若值为 true，则只能通过节点左侧图标来展开收缩
     *
     * @see $onlyIconControl
     * @see setOnlyIconControl()
     * @see getOnlyIconControl()
     */
    private $_onlyIconControl = false;

    /**
     * @var bool 是否允许点击节点时弹出新窗口跳转
     * @see $isJump
     * @see setIsJump()
     * @see getIsJump()
     */
    private $_isJump = false;

    /**
     * @var bool 是否开启节点连接线。若设为 false，则节点左侧出现三角图标。
     * @see $showLine
     * @see setShowLine()
     * @see getShowLine()
     */
    private $_showLine = true;

    /**
     * @var array 自定义 data 数据源中常用的字段名称。
     * @see $customName
     * @see setCustomName()
     * @see getCustomName()
     */
    private $_customName = [];

    /**
     * @var bool 是否显示全选/反选按钮
     * @see $switchCheckedBtn
     * @see setSwitchCheckedBtn()
     * @see getSwitchCheckedBtn()
     */
    private $_switchCheckedBtn = false;

    /**
     * @var bool 是否显示展开/折叠按钮
     * @see $switchSpreadBtn
     * @see setSwitchSpreadBtn()
     * @see getSwitchSpreadBtn()
     */
    private $_switchSpreadBtn = false;

    /**
     * @var array 自定义操作项(二维)
     * - position 按钮位置, 支持: top(默认), bottom
     * - title 按钮名称
     * - icon ICON图标, 支持bootstrap v4图标, fontawesome v4, layui 2.8图标
     * - btnClass 按钮class样式
     * - options 选项
     *
     * 支持的options选项有:
     * - type string 类型: page(页面), modal(模态框), ajax(AJAX); 默认modal
     * - route string 路由; 默认当前路由
     * - params array 参数, 默认值详看不同类型
     * - width string 当前type为modal时有效, 指定modal的宽;
     * - height string 当前type为modal时有效, 指定modal的高;
     * - closeBtn boolean 底部关闭按钮, 当type为modal时, 改配置生效
     * - target string 打开窗口: _self, _blank; type为page时, 改配置有效
     * - questionTitle string 询问标题, 当type为ajax时，该配置项有效
     * - questionDescription string 询问描述, 当type为ajax时，该配置项有效
     * - method string 访问动作; type为ajax有效, 只支持GET、POST; 默认GET
     * - closeModal boolean 是否关闭当前窗口层,用于当前树组件模态框展示时,ajax提交后自动关闭;type为ajax有效,默认false
     * - submittedReload boolean 是否提交后重载tree组件,type为ajax有效,默认false
     * - submittedCalls array 提交后的回调,空数组代表不回调;type为ajax有效,默认空数组,更多解释查看util/Ajax.php
     *  --- 隐藏项 ---
     * - __event_id 事件ID, 系统自动生成
     * - __mustSelectCheckbox 当params不为空时, 是否必须选择checkbox, 该参数根据params中的类型自动填充
     *
     * @see $operationItem
     * @see setOperationItem()
     * @see getOperationItem()
     */
    private $_operationItem = [];

    /**
     * @var array 自定义一些默认文本
     * @see setDefaultText()
     */
    private $_defaultText = [
        'defaultNodeName' => '未命名',
        'none' => '空',
    ];

    /**
     * @var array 额外的JS
     * ```php
     * $table->extraCss = <<<JS
     * console.log("测试一下 [setExtraCss] 方法;")
     * JS;
     *
     * ```
     * @see $extraJs
     * @see setExtraJs()
     * @see getExtraJs()
     */
    private $_extraJs = [];

    /**
     * @var array 额外的CSS
     * ```php
     * $table->extraCss = <<<CSS
     * body{background-color: #fff;}
     * CSS;
     *
     * ```
     * @see $extraCss
     * @see setExtraCss()
     * @see getExtraCss()
     */
    private $_extraCss = [];

    /**
     * @var array 额外的静态资源包
     * ```php
     * $table->extraAssetBundle = MemberAsset::class;
     *
     * ```
     * @see $extraAssetBundle
     * @see setExtraAssetBundle()
     * @see getExtraAssetBundle()
     */
    private $_extraAssetBundle = [];

    /**
     * @var bool 是否自动发起数据请求
     * @see $autoRequest
     * @see setAutoRequest()
     * @see getAutoRequest()
     */
    private $_autoRequest = true;

    /**
     * @var array tree组件切点内容
     * - key int 切点位置
     * - value array 切点html或text内容
     */
    private $_pointcutBody = [];

    /**
     * @var int 事件计数器
     */
    private static $_eventCounter = 0;

    /**
     * 设置视图局部继承
     * @param bool $boolean
     * @return $this
     */
    public function setPartial($boolean = true)
    {
        $this->_partial = $boolean;
        return $this;
    }

    /**
     * @return bool
     */
    public function getPartial()
    {
        return $this->_partial;
    }

    /**
     * 设置AJAX请求路由
     * @param string $route 路由
     * @param array $params 路由参数
     * @return $this
     */
    public function setAjaxRoute($route, $params = [])
    {
        $this->_ajaxRoute = [
            'route' => $route,
            'params' => (array)$params,
        ];

        return $this;
    }

    /**
     * @return string
     */
    public function getAjaxRoute()
    {
        $params = [
            $this->idParam => $this->id,
            App::params('http.ajax.identity') => 1,
        ];
        $ajaxRoute = $this->_ajaxRoute;

        if (empty($ajaxRoute)) {
            return Url::current($params, '');
        } elseif (empty($ajaxRoute['route'])) {
            $params = array_replace_recursive($ajaxRoute['params'], $params);
            return Url::current($params, '');
        } else {
            $ajaxRoute['params'][0] = $ajaxRoute['route'];
            $params = array_replace_recursive($ajaxRoute['params'], $params);

            return Url::to($params, '');
        }
    }

    /**
     * 设置标题
     * @param string $title
     * @return $this
     */
    public function setTitle($title)
    {
        $this->_title = (string)$title;
        return $this;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->_title;
    }

    /**
     * 设置根布局
     * @param array $options
     * @return $this
     */
    public function setRootLayout(array $options)
    {
        if (!empty($options['class'])) {
            $this->formatUiClass($options['class']);
        } else {
            $options['class'] = '';
        }

        if (!empty($options['style'])) {
            $this->formatStyle($options['style']);
        } else {
            $options['style'] = '';
        }

        if (!empty($options['attribute'])) {
            $this->formatHtmlAttribute($options['attribute']);
        } else {
            $options['attribute'] = '';
        }

        $this->_rootLayout = $options;
        return $this;
    }

    /**
     * @return array
     */
    public function getRootLayout()
    {
        if (empty($this->_rootLayout)) {
            return [
                'style' => '',
                'class' => '',
                'attribute' => '',
            ];
        }

        return $this->_rootLayout;
    }

    /**
     * 设置树布局
     * @param array $options
     * @return $this
     */
    public function setTreeLayout(array $options)
    {
        if (!empty($options['class'])) {
            $this->formatUiClass($options['class']);
        } else {
            $options['class'] = '';
        }

        if (!empty($options['style'])) {
            $this->formatStyle($options['style']);
        } else {
            $options['style'] = '';
        }

        if (!empty($options['attribute'])) {
            $this->formatHtmlAttribute($options['attribute']);
        } else {
            $options['attribute'] = '';
        }

        $this->_treeLayout = $options;
        return $this;
    }

    /**
     * @return array
     */
    public function getTreeLayout()
    {
        if (empty($this->_treeLayout)) {
            return [
                'style' => '',
                'class' => '',
                'attribute' => '',
            ];
        }

        return $this->_treeLayout;
    }

    /**
     * 设置ajax返回数据
     * @param array|\Closure $data
     * @return $this
     */
    public function setData($data)
    {
        empty($data) && $data = [];
        if (!is_array($data) && !is_callable($data)) {
            throw new InvalidArgumentException('Expected parameter type is array or callable, But the others are given. ');
        }

        $this->_data = $data;
        return $this;
    }

    /**
     * @return array
     */
    public function getData()
    {
        if (is_callable($this->_data)) {
            $data = call_user_func($this->_data);
            empty($data) && $data = [];
            if (!is_array($data)) {
                throw new InvalidArgumentException('Expected parameter type is array, But the others are given.');
            }

            return $data;
        }

        return $this->_data;
    }

    /**
     * 设置是否显示复选框
     * @param bool $boolean
     * @return $this
     */
    public function setShowCheckbox($boolean = true)
    {
        $this->_showCheckbox = (boolean)$boolean;
        return $this;
    }

    /**
     * @return bool
     */
    public function getShowCheckbox()
    {
        return $this->_showCheckbox;
    }

    /**
     * 设置是否开启手风琴模式
     * @param bool $boolean
     * @return $this
     */
    public function setAccordion($boolean = true)
    {
        $this->_accordion = (boolean)$boolean;
        return $this;
    }

    /**
     * @return bool
     */
    public function getAccordion()
    {
        return $this->_accordion;
    }

    /**
     * 设置是否仅允许节点左侧图标控制展开伸缩
     * @param bool $boolean
     * @return $this
     */
    public function setOnlyIconControl($boolean = true)
    {
        $this->_onlyIconControl = (boolean)$boolean;
        return $this;
    }

    /**
     * @return bool
     */
    public function getOnlyIconControl()
    {
        return $this->_onlyIconControl;
    }

    /**
     * 设置是否允许点击节点时弹出新窗口跳转
     * @param bool $boolean
     * @return $this
     */
    public function setIsJump($boolean = true)
    {
        $this->_isJump = (boolean)$boolean;
        return $this;
    }

    /**
     * @return bool
     */
    public function getIsJump()
    {
        return $this->_isJump;
    }

    /**
     * 设置是否开启节点连接线
     * @param bool $boolean
     * @return $this
     */
    public function setShowLine($boolean = true)
    {
        $this->_showLine = (boolean)$boolean;
        return $this;
    }

    /**
     * @return bool
     */
    public function getShowLine()
    {
        return $this->_showLine;
    }

    /**
     * 自定义 data 数据源中常用的字段名称。
     * @param array $options
     * @return $this
     */
    public function setCustomName(array $options)
    {
        $this->_customName = $options;
        return $this;
    }

    /**
     * @return array
     */
    public function getCustomName()
    {
        return $this->_customName;
    }

    /**
     * 设置是否显示全选/反选按钮
     * @param bool $boolean
     * @return $this
     */
    public function setSwitchCheckedBtn($boolean = true)
    {
        $this->_switchCheckedBtn = $boolean;
        return $this;
    }

    /**
     * @return bool
     */
    public function getSwitchCheckedBtn()
    {
        return $this->_switchCheckedBtn;
    }

    /**
     * 设置是否显示展开/折叠按钮
     * @param bool $boolean
     * @return $this
     */
    public function setSwitchSpreadBtn($boolean = true)
    {
        $this->_switchSpreadBtn = $boolean;
        return $this;
    }

    /**
     * @return bool
     */
    public function getSwitchSpreadBtn()
    {
        return $this->_switchSpreadBtn;
    }

    /**
     * 设置自定义操作项(二维)
     * @param array $data
     * @return $this
     */
    public function setOperationItem(array $data)
    {
        $result = [];
        foreach ($data as $key => $item) {
            if (is_callable($item)) {
                $item = call_user_func($item);
            } elseif (is_object($item) && $item instanceof OperationType) {
                $item = $item->_toArray();
            }

            if (!is_array($item)) {
                throw new InvalidArgumentException('Expected parameter type is array, But the others are given. ');
            }

            if (empty($item)) {
                continue;
            }

            // 默认值: common
            empty($item['position']) && $item['position'] = 'top';
            empty($item['title']) && $item['title'] = 'custom';
            empty($item['icon']) && $item['icon'] = '';

            if (!empty($item['btnClass'])) {
                $this->formatUiClass($item['btnClass']);
            } else {
                $item['btnClass'] = 'layui-btn-theme';
            }

            if (empty($item['options']) || !is_array($item['options'])) {
                throw new InvalidArgumentException('The parameter [options] is empty or not array.');
            }

            // 默认值: common
            empty($item['options']['__mustSelectCheckbox']) && $item['options']['__mustSelectCheckbox'] = false;
            if (empty($item['options']['params'])) {
                $item['options']['params'] = [];
            } else {
                $item['options']['params'] = (array)$item['options']['params'];
            }

            if (empty($item['options']['route'])) {
                $item['options']['route'] = Url::current([], '');
            } else {
                $item['options']['route'] = Url::to($item['options']['route'], '');
            }

            // 默认值: page
            empty($item['options']['target']) && $item['options']['target'] = '_self';

            // 默认值: ajax
            empty($item['options']['method']) && $item['options']['method'] = 'GET';
            empty($item['options']['questionTitle']) && $item['options']['questionTitle'] = '';
            empty($item['options']['questionDescription']) && $item['options']['questionDescription'] = '确定要执行该操作么?';

            // 默认值: modal
            empty($item['options']['type']) && $item['options']['type'] = 'modal';
            empty($item['options']['width']) && $item['options']['width'] = '620px';
            empty($item['options']['height']) && $item['options']['height'] = '750px';
            empty($item['options']['closeBtn']) && $item['options']['closeBtn'] = false;

            // 事件ID
            $item['options']['__event_id'] = $this->_generateEventId();

            $result[$key] = $item;
        }

        $this->_operationItem = array_merge($this->_operationItem, $result);
        return $this;
    }

    /**
     * @return array
     */
    public function getOperationItem()
    {
        return $this->_operationItem;
    }

    /**
     * 设置一些自定义默认文本
     * @param string $noneTips 空数据显示文本
     * @param string $defaultNodeName 新增的默认节点名
     * @return $this
     */
    public function setDefaultText($noneTips = '空', $defaultNodeName = '未命名')
    {
        $this->_defaultText = [
            'defaultNodeName' => (string)$defaultNodeName,
            'none' => $noneTips,
        ];

        return $this;
    }

    /**
     * 设置额外的JS
     * @param array|string $js 额外的JS脚本
     * @return $this
     */
    public function setExtraJs($js)
    {
        $js = (array)$js;
        foreach ($js as &$value) {
            if (is_callable($value)) {
                $value = call_user_func($value);
            }
        }

        $this->_extraJs = array_merge($this->_extraJs, $js);
        return $this;
    }

    /**
     * 获取额外的JS
     * @return array
     */
    public function getExtraJs()
    {
        return $this->_extraJs;
    }

    /**
     * 设置额外的CSS
     * @param array|string $css 额外的CSS脚本
     * @return $this
     */
    public function setExtraCss($css)
    {
        $css = (array)$css;
        foreach ($css as &$value) {
            if (is_callable($value)) {
                $value = call_user_func($value);
            }
        }

        $this->_extraCss = array_merge($this->_extraCss, $css);
        return $this;
    }

    /**
     * 获取额外的CSS
     * @return array
     */
    public function getExtraCss()
    {
        return $this->_extraCss;
    }

    /**
     * 设置额外的静态资源包
     * @param string|array $extraAssetBundle 静态资源包
     * @return $this
     */
    public function setExtraAssetBundle($extraAssetBundle)
    {
        $extraAssetBundle = (array)$extraAssetBundle;

        // 检查
        foreach ($extraAssetBundle as $assetBundle) {
            $tempObj = new $assetBundle;
            if (!$tempObj instanceof AssetBundle) {
                throw new InvalidArgumentException($assetBundle . ' is not instance of ' . AssetBundle::class);
            }
        }

        $this->_extraAssetBundle = array_merge($this->_extraAssetBundle, $extraAssetBundle);
        return $this;
    }

    /**
     * 获取额外的静态资源包
     * @return array
     */
    public function getExtraAssetBundle()
    {
        return $this->_extraAssetBundle;
    }

    /**
     * 是否自动发起数据请求
     * @param bool $boolean
     * @return $this
     */
    public function setAutoRequest($boolean = true)
    {
        $this->_autoRequest = (bool)$boolean;
        return $this;
    }

    /**
     * @return bool
     */
    public function getAutoRequest()
    {
        return $this->_autoRequest;
    }

    /**
     * 注册tree组件切点
     * @param string $content 内容
     * @param int $pos 切点
     * @return $this
     */
    public function registerPointcut($content, $pos = self::POS_HEAD)
    {
        $this->_pointcutBody[$pos][] = (string)$content;
        return $this;
    }

    /**
     * 开始tree
     * @return null|string
     */
    public function beginTree()
    {
        if (empty($this->_pointcutBody[self::POS_HEAD])) {
            return null;
        }

        return implode("\n", $this->_pointcutBody[self::POS_HEAD]);
    }

    /**
     * 结束tree
     * @return null|string
     */
    public function endTree()
    {
        if (empty($this->_pointcutBody[self::POS_END])) {
            return null;
        }

        return implode("\n", $this->_pointcutBody[self::POS_END]);
    }

    /**
     * 实例化
     * @param array $config
     * @return TreeBuilder
     */
    public static function instance($config = [])
    {
        return new static($config);
    }

    /**
     * @return string the view path that may be prefixed to a relative view name.
     */
    public function getViewPath()
    {
        return __DIR__;
    }

    /**
     * 构建器名
     * @return string
     */
    public function getName()
    {
        return 'tree';
    }

    /**
     * 树组件渲染
     * @param bool $onlyRenderContent 是否仅渲染内容
     * @return string
     * @throws \yii\base\InvalidConfigException
     */
    public function render($onlyRenderContent = false)
    {
        if ($this->isAjax) {//AJAX
            return $this->asOk('success', $this->data);
        } else {//HTML
            $content = $this->_renderHtml();

            // 仅内容渲染
            if ($onlyRenderContent) {
                return $content;
            }

            // 局部布局渲染(不包含: 菜单和导航栏)
            if ($this->partial) {
                return $this->_view->renderFile($this->partialPath, ['content' => $content], $this);
            }

            // 主布局渲染(包含: 菜单和导航栏)
            return $this->_view->renderFile($this->mainPath, ['content' => $content], $this);
        }
    }

    /**
     * 渲染HTML
     * @return string
     * @throws \yii\base\InvalidConfigException
     */
    private function _renderHtml()
    {
        if ($this->title) {
            // 注册页标题
            $this->_view->title = H5::encode(App::params('admin.brandName') . '|' . $this->title);
        }

        // 注册JS脚本
        $this->_view->registerJs($this->_resolveJsScript(), View::POS_END);

        // 注册额外CSS脚本
        if (!empty($this->extraCss)) {
            $this->_view->registerCss(implode("\n", $this->extraCss));
        }

        // 注册额外的AssetBundle
        if (!empty($this->extraAssetBundle)) {
            foreach ($this->extraAssetBundle as $assetBundle) {
                $this->_view->registerAssetBundle($assetBundle);
            }
        }

        $content = $this->_view->render('view', [
            // view.php中参数注册
            'rootLayout' => $this->rootLayout,//根布局
            'treeLayout' => $this->treeLayout,//树布局
            'switchCheckedBtn' => $this->switchCheckedBtn,//是否显示全选/反选按钮
            'switchSpreadBtn' => $this->switchSpreadBtn,//是否显示展开/折叠按钮
            'operationItem' => ArrayHelper::index($this->operationItem, null, 'position'),//自定义操作项(分组)

        ], $this);

        return $content;
    }

    /**
     * 解析树组件的JS脚本
     * @return string
     */
    private function _resolveJsScript()
    {
        $jsScripts = $this->_view->render('app', [
            // app.php中参数注册
            'showCheckbox' => $this->showCheckbox, //是否显示复选框
            'accordion' => $this->accordion,//是否开启手风琴模式
            'onlyIconControl' => $this->onlyIconControl,//是否仅允许节点左侧图标控制展开伸缩
            'isJump' => $this->isJump,//是否允许点击节点时弹出新窗口跳转。若为 true，则需在对应的 data 中设定 href 属性（url 格式）
            'showLine' => $this->showLine,//是否开启节点连接线。若设为 false，则节点左侧出现三角图标。
            'customName' => $this->customName,//自定义 data 数据源中常用的字段名称
            'autoRequest' => $this->autoRequest,//是否自动发起数据请求
            'ajaxRoute' => $this->ajaxRoute,//AJAX请求路由
            'defaultText' => $this->_defaultText,//自定义一些默认文本
            'operationItemRaw' => $this->operationItem,//自定义操作项(未分组)
            'extraJs' => $this->extraJs,//额外的JS脚本

        ], $this);

        return RegEx::js($jsScripts);
    }

    /**
     * 生成事件ID
     * @return int
     */
    private function _generateEventId()
    {
        self::$_eventCounter++;
        return 'ym2_tree_event' . self::$_eventCounter;
    }
}